Node.js SSL/ HTTPS

HTTP(Hyper Text Transfer Protocol,超文本传输协议)是一个被互联网广泛使用的协议之一,所有 WWW 文件都必须遵守的标准,但缺点是 HTTP 传输数据是不会被加密的。因此 HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer,安全的超文本传输协议) 的出现解决了这个问题,他主要使用 SSL(Secure Sockets Layer,安全套接层协议)用于对 HTTP 传输协议进行加密,以保证会话过程中的安全性。

由于 SSL 协议中主要通过对称加密和非对称加密两种,在建立链路后,SSL 内容会使用其对称加密的公钥对其进行非对称加密,之后 SSL 会对内容进行对称加密。

OpenSSL

由于 TLS\SSL 是非对称加密,因此在此之前需要准备加密解密和验证的私钥以及数字证书、和服务器、客户端的密钥证书

1
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost'  -keyout localhost-privkey.pem -out localhost-cert.pem

HTTPS

.

1
2
3
4
5
6
7
8
9
10
11
12
13
const https = require('https')
const fs = require('fs')

const options = {
key: fs.readFileSync('./ssl/localhost-privkey.pem'),
cert: fs.readFileSync('./ssl/localhost-cert.pem')
}

https.createServer(options, (req,res) => {
res.writeHead(200)
res.write('<h1>Hello,world!</h1>')
res.end()
}).listen(8210)

之后我们访问 https://localhost 同样会提示证书的问题,因为我们这个是自己生成的并不是以权威机构签名,因此在浏览器中会报错。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js TCP Server

TCP Server 通过 Node 中的 net 模块来实现 TCP 四层模型(应用层、传输层、网络层、链路层)

Id Name Info Protocol
1 链路层(Link layer) 负责在以太网、Wifi 底层网络发送数据包,并通过网卡、使用 MAC 地址来标记网络上的设备 GPRS、IEEE、IPoe、Localhost、TRILL、IPoAC、FDDI、NAK\NACK、SEND、frame\frame relay、ATM、Data Link Layer、PPP、STP、L2TP、ARQ、CDPD、LLDP、LCP、Link Aggregation、HDLC
2 网络层\网络互联层(Internet layer) 定义了 IP 地址的概念,通过 IP 地址取代 MAC 地址,并将局域网、广域网链接成一个虚拟网络,主要目的就是 将 IP 地址翻译成 MAC 地址就可查找设备 IPsec、IPv4\IPv5\IPv6\IPv9、 IP Address、x.25、IPX、ICMP\ICMPv6、IGMP、DDP、Mobile IP、Network Layer、PPP、RSVP、Anti-replay
3 传输层(Transport layer) 保证数据在 IP 地址标记的两点之间进行传输 TCP、UDP、TLS\SSL、DCCP、SCTP、RSVP
4 应用层(Application layer) 直接与应用程序和接口结合,并提供常见的应用服务 DHCP、DNS、FTP、gopher、HTTP\HTTP-2、IMAP4、IRC、NNTP、XMPP、POP3、SIP、SMTP、SNMP、SSH、TELNET、RPC、RTCP、RTP、RTSP、SDP、SOAP、GTP、STUN、NTP、SSDP

对于每个网络层次,Node 都提供了相应的模块,如 http、net、tls/crypto、dgram 等,其中 net、http、dgram 模块分别实现和提供 TCP、HTTP 的通信,其中 http 为应用层模块,而 net 为传输层模块。

.http 是一个综合模块,包含了 http/tls/crypto 等,这将用于确保传输的安全性。其中 net 模块是 node 中的核心模块,严谨的来说 http.Server 继承了 net.Server,除此之外 HTTP SERVER/CLIENT 都以来与 net.Socket,而 net 模块主要分为:

  1. net.Server:通过其内部的 socket 来提供客户端的通信
  2. net.Socket:TCP/Localhost socket 的实现,并提供了全双工的 stream 接口

Network socket (网络套接字)是指网络中不同主机上应用程序之间的通信端点对象,一个套接字就是一个的另一端,也就是以 IP 及端口所组成的地址被称之为套接字地址(socket address)

Create TCP Server

1
2
3
4
5
6
7
8
9
var net = require('net')

var server = net.createServer(function (socket) {
console.log('someone sonnects')
})

server.listen(8210, function () {
console.log('Create server on http://127.0.0.1:8210')
})

创建一个 TCP 服务可以通过 net.createServer 方法进行创建,可通过 server.listen 来设置服务的监听端口以及,当然一旦运行服务时除发的还是 server 下的 listening 事件,因此也支持另一种写法:

net.Server()

1
2
3
4
5
6
7
8
9
10
11
var net = require('net')

var server = net.createServer(function (socket) {
console.log('someone sonnects')
})

server.listen(8210)

server.on("listening", function () {
console.log("Create server on http://127.0.0.1:8210/")
})

除此之外 server 还支持另外几种 TCP 事件:

Id Name Info
1 listening 调用 server.listen(),即当开始监听时候除发
2 connection 当有新创建时后除发,回调的参数是 socket 连接的对象
3 close 当关闭时触发,回调函数没有参数
4 error 当 TCP 服务发生错误时触发,回调参数为 error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var net = require('net')

var server = new net.Server()

// 监听新的请求时返回回调函数
server.on("connection", function (socket) {
console.log("connects someone")
})

server.listen(8210)

// 监听时触发
server.on("listening", function () {
console.log("Create server on http://127.0.0.1:8210/")
})

// 关闭触发回调函数
server.on("close", function () {
console.log("close server")
})

// 错误时返回
server.on("error", function (err) {
console.log("server error!")
})

server.address()

如果需要监听 IP 套接字,也就是套接字地址(socket address),可以通过 server.address() 方法来实现,他主要提供了三个服务器绑定的 address 以及 familyport

1
2
3
4
5
6
7
8
9
10
11
12
var net = require('net')

var server = net.createServer(function (socket) {
console.log("connects someone")
})

server.listen(8210, function () {
var address = server.address()
console.log("TCP Server 所监听的端口号:" + address.port)
console.log("TCP Server 所监听的地址:" + address.address)
console.log("TCP Server IPv4 or IPv6:" + address.family)
})

需要注意的是如果监听的端口号是 IPv6 地址,那么 address 则返回 ::

server.getConnections()

.server.getConnections 方法用于异步获取服务器上的并发连接数,回调参数分为 errcount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const net = require("net")

/*
Create server on http://127.0.0.1:8210
someone connects
This is client count: 1
*/
var server = net.createServer(function (socket) {
console.log('someone connects')

server.maxConnections=3
server.getConnections(function (err,count) {
console.log("This is client count: " + count)
})
})

server.listen(8210, function () {
console.log("Create server on http://127.0.0.1:8210")
})

之后我们分别通过 GET 请求 http://127.0.0.1:8210/ 并通过代理访问 uri 则有三个 IP 访问,因此分别依次循环输出 This is Client count: 1~3

Server at Client 之间的通信

Server

服务端主要用于接收客户端所发送的消息,因此只需要通过 data 事件来接收数据时进行输出,在收到数据之前首先通过 address() 来将转换为 JSON 格式并输出,并通过 socket.bytesWritten 来计算客户端所发送的字节数,最后通过 socket.bytesRead 方法来统计书数据的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const net = require('net')

/*
Create server on http://127.0.0.1/8210
The is address is {"address":"::","family":"IPv6","port":8210} 以发送
消息大小为 62
Massage from client
数据的大小为:19
*/
var server = net.createServer(function (socket) {
var addresss = server.address()
var message = "The is address is " + JSON.stringify(addresss)

socket.write(message,function () {
var writeSize = socket.bytesWritten
console.log(message + " 以发送")
console.log("消息大小为 " + writeSize)
})

// 收到数据时回调
socket.on('data', function (data) {
console.log(data.toString())
var readSize = socket.bytesRead
console.log("数据的大小为:" + readSize)
})
})

server.listen(8210,function () {
console.log("Create server on http://127.0.0.1/8210")
})

Client

在 Server 与 Client 建立链接的过程中,客户端扮演的是数据发送者的身份,而他主要的作用就是与服务端建立链接并发送数据,之后监听服务端的状态,在这期间主通过 client.connect 来链接服务,之后使用其 client.write 来写入数据并发送。

net.Socket 类

API

对于 net.Socket 相关的 API Node 提供了多种方法:

Id Name Info
1 socket.address() 返回操作系统报告并绑定 address,和套接字的 port 以及 family 等
2 socket.bytesRead() 接收的字节数
3 socket.bytesWritten() 发送的字节数
4 socket.connect(options[,sonnectListener]) 指定套接字链接
options
port 套接字应该链接到的端口(必填)
host 套接字应连接到的主机(默认为 localhost
localAddress 套接字应该链接到本地地址
localPort 套接字应链接本地端口
family IP 堆栈的版本(通常是 IPv6、IPv4或0,而 0 表示 允许 IPv4/6 地址)
hints 可选的 dns.lookup() 提示(主机名)
lookup 自定义查找函数(默认 dns.lookup()
5 socket.connect(port[,host][,connectListener]) 指定套接字链接
port 客户端应该链接到的端口
host 客户端应该链接到的主机
connectListener 方法的常用参数,将被添加为 connect 事件监听
6 socket.destroyed 半关闭套接字,服务端关闭时发送数据包给客户端,客户端关闭
7 socket.localAddress 远程客户端链接本地 IP 地址(以 IP 地址的字符串形式表示,如 0.0.0.0
8 socket.localPort(socket.remotePort) 本地端口数字表示(如 8210
9 socket.remoteAddress 远程 IP 地址的字符串形式输出(如 74.125.127.1002001:4860:a005::68
10 socket.remoteFamily 远程 IP 系列的字符串形式表示(如 IPv6IPv4
11 socket.setTimeout(socket.timeout) 用于进行设置连接(如果 timeout 为 0,则禁用空闲超时)
12 socket.setKeepAlive() 用于设置和i长连接
13 socket.destroy()\socket.destroyed 当错误发生时,用于销毁 socket,以确保这个 socket 上不会有其他 IO 操作
事件
Id Name Info
1 close 当链接断开时触发,假设因为传输错误导致断开,则参数为 err
2 connect 当建立成功套接字链接时触发
3 data 接收到数据时触发
4 drain 当写缓存空了时触发
5 end 当另一端结束时触发
6 error 当错误时触发
7 lookup 当解析完主机名后触发
8 ready 当套接字准备好使用时触发
9 timeout 提示用户 socket 超时,需要手动关闭链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const net = require('net')

/*
Connnect to Server
The is address is {"address":"::","family":"IPv6","port":8210}
*/
var client = new net.Socket()

// 链接服务器,"connect" 是指当成功建立连接时触发
client.connect(8210,'127.0.0.1', function () {
console.log("Connnect to Server")

// 与 Server 建立链接
client.write("Massage from client")
})

client.on("data", function (data) {
console.log(data.toString())
})

// 当套接字另一端断开链接时出发回调,从而结束套接字可读端
client.on("end", function () {
console.log("end Server")
})

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js Express at ejs

express 是一个高度包容和快速包容且极简的 Node.js Web 框架,主要用于其 API 路由以及对数据的集成支持,可通过 express 模块完成各种使用程序方法和中间件,方便快捷的创建一个强大的 API。

通过 express 和模板引擎 ejs 的配合,以 ejs 十分简单以及对 express 良好的集成,可以充分的完成前后端的相关开发需求,并以此来完成一个非常规范的 node start 写法,在此之前我们需要安装 express 以及 ejs 模块

npm install ejs \ npm install express

路由用于确定应用程序特定端点的客户端请求,主要通过 express 来进行实现,而这其中 app 与 HTTP 方法相对应的 Express 对象方法来定义路由,如 app.get() 用于处理 GET 请求,而 app.post 则用于处理 POST 请求。

Express

这些路由方法都指定了回调函数(或者:“处理程序函数”),当程序接收到指定的路由(端点)的时候(也就是说 HTTP 方法请求时被调用),来调用回调函数,换句话说就是应用程序监听与指定路由和方法匹配的请求,当检测到匹配时,他会调用对应的回调函数。

比如我们要创建一个非常符合 node 风格的项目并搭配 ejs 进行开发,可以目录结构可以为:

1
2
3
4
5
6
7
8
node/
├── server.js
├── package.json
├── package-lock.json
├── route
│ └── index.js
└── views
└── index.ejs

除去 node_modules 目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var path = require('path')
var express = require('express')
var app = express()

var indexRoute = require('./route/index')

// app.set(name, value),将 set name 分配给 value,因此他可以存储任何想要的值
app.set('views', path.join(__dirname, 'views'))

// 指定渲染引擎使用使用 ejs
app.set('view engine', 'ejs')

// 路由将匹与 path 相同的路径
app.use('/', indexRoute)

app.listen(8021)

路由参数

接下来在 route/ 目录里面添加 use 所对应的文件,并引入 express 库,并通过 express.Router 创建模块化,之后以 router.get 实例来检测端点参数的访问,之后通过 res.render 将 HTML 字符串发送到客户端:

  • res.render(view[, locals][, callback])
    • view 参数是一个字符串,是视图文件,用于渲染文件的路径
    • locals,一个对象,其属性定义了视图的局部变量。
    • callback,回调函数。如果提供,该方法将返回可能的错误和呈现的字符串,但不执行自动响应。当发生错误时,该方法会在next(err)内部调用。
1
2
3
4
5
6
7
8
9
10
var express = require('express')
var router = express.Router()

router.get('/:name', function (req,res) {
res.render('index', {
name: req.params.name
})
})

module.exports = router

在上述 code 中,在 router.get 监控路由端点中所使用的路由参数(:name)是一个包含映射命名路由,其作用是捕获 URL 中位置所指定的值,并填充在 req.params 对象中,你可以随意定义:

1
2
3
4
5
6
7
8
9
10
11
var express = require('express')
var router = express.Router()

router.get('/user/:username/uid/:userid', function (req,res) {
res.render('index', {
username: req.params.username,
userid: req.params.userid
})
})

module.exports = router

http://localhost:8021/user/kunlun/uid/20


当然如果不需要 ejs 进行交互也可以通过其 res.render 方法 options参数来进行实现:

1
2
3
4
5
6
7
8
9
10
var express = require('express')
var router = express.Router()

router.get('/:name', function (req,res) {
res.render('index', function (err,html) {
res.send(html)
})
})

module.exports = router

http://localhost:8021/hehe

在这其中 res.send 是一个用于发送 HTTP 响应的方法,所描述的 body 可以是 buffer、string\boolean\array 对象等。

Ejs

ejs 试图中非常简单,我们只需要掌握 ejs 是那个非常关键的几个语法就可以进行使用:

Id Name Info
1 <% code %> 用于运行 JavaScript 代码
2 <%= code %> 显示转义后的 HTML 内容
3 <%- code %> 显示原始的 HTML 内容
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>DOCTYPE</title>
</head>
<body>
<h1>User: <%= username%></h1>
<p>Uid: <%= userid%></p>
</body>
</html>

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js HTTP Server

HTTP(Hyper Text Transfer Protocol)即浏览器和服务器之间的传输协议,用于浏览器和服务器之间的通信问题,从握手开始处理 TCP 链接,为实现解析 HTTP 的问题,Node 可以通过其内置的 http 模块来解决此问题。

应用程序并不可以直接和 HTTP 进行一些操作,因此可以通过其 http 模块中所提供的 require(请求) 以及 response(响应) 来将 HTTP 响应返回服务器。

因为 require 封装了 HTTP 请求,因此可以通过使用 require 对象属性方法获取所有的 HTTP 请求信息

同样的因为 response 对象封装了 HTTP 响应,也可以通过此对象方法将 HTTP 响应返回给浏览器

因此 http 模块也可以有两种方式进行使用,服务端创建 HTTP 服务器以此来监听客户端的请求并返回响应,而作为客户端,将发起一个 HTTP 请求(客户端)来获取服务端的响应。

Request at Response

Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http')

// 创建本地服务器来从其接收数据
var server = http.createServer(function (req,res) {
// 获得 HTTP 请求的 method + url
console.log(req.method + ':' + req.url)
req.on('data', function (chunk) {
body.push(chunk)
})

// 向请求发送响应头
res.writeHead(200, {'Content-Type':'text/html'});
// 完成并发送请求
res.end('<h1>Hello,world!</h1>')
})

server.listen(8081)

console.log('Server is running at http://localhost:8081/');

HTTP 请求的本质上就是 Headers 以及 body 两者组成,因此我们首先通过了 http.createServer 来创建一个服务器,然后通过 server.listen() 的方法监听端口,当客户端发出请求时,服务器就会的回调函数就会i被调用,因此输出:

1
2
3
4
5
Server is running at http://localhost:8081/
GET:/
GET:/favicon.ico
GET:/
GET:/favicon.ico

HTTP 请求在发送服务器时,是一个字节一个字节的以数据流方式发送,HTTP 模块创建的 HTTP 服务器接受到完整的请求头后会调用函数,因此除了可以使用 request 对象访问请求头数据以外,还可以将此当作一个只读数据流来访问请求数据提

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http')

http.createServer(function (req,res) {
var body = []

console.log(req.method)
console.log(req.headers)

req.on('data', function (chunk) {
body.push(chunk)
})

req.on('end', function () {
body = Buffer.concat(body)
console.log(body.toString())
})
}).listen(8081)

因此输出的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET
{ host: 'localhost:8081',
connection: 'keep-alive',
'sec-ch-ua':
'"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"',
'cache-control': 'no-cache',
'sec-ch-ua-mobile': '?0',
'user-agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
'postman-token': '9c86d3ea-3df2-9485-75e6-f8d2af8c0b03',
accept: '*/*',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6' }

Response

客户端发送请求并接受到完整的服务端响应头时,就会调用回调函数,在回调函数中,可以通过 response 对象访问响应头外,还可以将 Response 当作一个只读数据流,其中最为主要的是 http.ServerResponse 所提供的类方法:

Id Name Info
1 response.statusCode HTTP 响应状态码
2 response.headers HTTP 响应头
3 response.statusMessage 当前消息状态
4 response.complete 是否已经接收到了完整的 HTTP 请求头信息
5 response.httpVersion 链接到服务器的HTTP 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const http = require('http')

/*
HTTP 状态码: 200
<h1>Hello,world!</h1>
请求头: true
*/
http.get('http://localhost:8081/', function (response) {
var body = []

console.log("HTTP 状态码: ",response.statusCode)

response.on('data', function (chunk) {
body.push(chunk)
})

response.on('end', function () {
body = Buffer.concat(body)
console.log(body.toString())
console.log("是否已经接收完整的 HTTP 信息:" ,response.complete)
})
})

HTTP/2


HTTP/2 是于 19999 年的 HTTP 1.1 的改进版(RFC 2616)后在 2014 年 12 月由 互联网工程任务组(IETF)内的 HTTPBIS(Hypertext Transfer Protocol Bis) 小组进行开发,并由 IETF 递交给 IESG 进行讨论,在 2015 年 5 月 正式批准(RFC 7540)

于 HTTP 1.1 相比支持了多路复用(Multiplexing)、首部压缩(Headers)、服务推送(Server Push) 等特点,有效减少了大量的开销。

服务器推送即允许服务器在客户端缓存中填充数据,通过服务器推送的机制来提前进行请求,但需要安装 http 2 模块

由于 http2 是一个内置模块,与 npm 上的 http2 无关

1
2
3
4
5
6
7
8
9
10
node/
├── package.json
├── package-lock.json
├── public
├── src
│ └── server.js
└── ssl
├── localhost-cert.pem
└── localhost-privkey.pem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const http2 = require('http2');
const express = require('express')
const fs = require('fs');

const server = http2.createSecureServer({
key: fs.readFileSync('./ssl/localhost-privkey.pem'),
cert: fs.readFileSync('./ssl/localhost-cert.pem')
});
server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
// stream is a Duplex
stream.respond({
'content-type': 'text/html; charset=utf-8',
':status': 200
});
stream.write("<h1>Hello,world!</h1>")
stream.end();
});

server.listen(8443);

之后即可通过 https://localhost:8443/ 直接进行访问,当然由于使用了 ssl 的原因 node 关闭了 http 的访问,因此只可以通过 https 进行,如果是自己生成的证书可能会报错,点击继续访问即可。

可直接通过 openssl 生成自己的本地证书

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj ‘/CN=localhost’
-keyout localhost-privkey.pem -out localhost-cert.pem

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js File System

Node.js fs(file system) 即是内置的文件系统模块,用于负责文件的到做以及读取等,可通过 console fs = require("fs") 直接进行引入,因此为了更详细的分清该模块所提供的 API,主要分为:“文件属性读写、文件内容读写、文件操作” 等三种。

fs 模块所提供的方法最大不同之处在于同时还提供了 异步、同步 两个方法,这体现在了 fs.readFile()fs.readFileSync()

.fs.readFile(path[, options], callback) 异步读取文件的全部内容
.fs.readFileSync(path[, options]) 用于返回 path 内容即同步

Id Name Info Type
1 fs.readFile(path[, options], callback) 用于异步读取文件 文件内容读写
2 fs.readFileSync(path[, options]) 同步读取文件 文件内容读写
3 fs.read(fd, buffer, offset, length, position, callback) 在异步模式下更加合理的读取文件 文件内容读写
fd 通过 fs.open() 所返回的文件描述
buffer 数据写入缓冲区
offset 缓冲区写入数据写入的偏移量
length 需要读取的字节数
position 文件读取的起始位置
callback 回调函数
err 返回错误信息
bytesRead 所读取的字节数
buffer 缓冲区对象
4 fs.open(path[, flags[, mode]], callback) 异步打开文件 底层文件操作
path 文件路径
flags 文件的打开行为
a 如果文件不存在则创建文件
ax a 的基础上,如果文件路径不存在,就失败
a+ 如果文件不存在就创建
ax+ a+ 的基础上,如果文件路径不存在,则读取失败
r 以读取的方式打开文件,如果文件不存在则异常
r+ 以读写模式打开文件,如果文件不存在就抛出异常
rs 以同步的方式读取文件
rs+ 以同步的方式读取和写入文件
w 以写入模式打开文件,文件不存在则创建
wx w 的基础上,如果文件路径存在,文件将会写入失败
w+ 以读写模式打开文件,如果文件不存在则创建
wx+ w+ 的基础上,如果文件路径存在,则文件写入失败
mode 打开的权限
callback 回调参数
5 fs.writeFile(file, data[, options], callback) 通过异步的方式写入文件 文件内容读写
file 文件名
data 需要写入的数据
options 一个参数对象,包含了编码模式(utf-8)以及 flags(w,写入模式打开文件,文件不存在则创建)
6 fs.writeFileSync() 通过同步的方式写入文件 文件内容读写
7 fs.stat() 用于读取文件属性 文件属性读写

readFile at readFileSync 读取文件

fs.readFile()

1
2
3
4
5
6
7
8
9
10
const fs = require('fs')

fs.readFile('data.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err)
} else {
// File system!
console.log(data.toString())
}
})

data.txt

File system!

.fs.readFile()fs.readFileSync() 的主要区别就是一个支持异步而另一个仅仅是同步方法,这主要可以根据其 callback() 就可以分辨出两者的作用与功能上的区分。

这主要涉及到 errdata 两个参数,后者为 undefined,也就是其 path 内的数据以字符串的形式进行读取,当发生错误时将会输出 err 对象。

fs.readFIleSync()

1
2
3
4
5
6
const fs = require('fs')

var data = fs.readFileSync('data.txt', 'utf-8')

// File system!
console.log(data)

通常默认情况下单个 fs.readFIleSync() 参数是不支持输出错误的,因此我们还需要输出函数的方式输出数据,可配合 try...catch 的方式来实现错误输出:

1
2
3
4
5
6
7
8
const fs = require('fs')

try {
var data = fs.readFileSync('data.txt', 'utf-8')
console.log(data)
} catch (err) {
console.log(err)
}

fs.read(fd, buffer, offset, length, position, callback) at fs.open and fs.close

Id Name Info Type
1 fs.read(fd, buffer, offset, length, position, callback) 在异步模式下更加合理的读取文件 文件内容读写
fd 通过 fs.open() 所返回的文件描述
buffer 数据写入缓冲区
offset 缓冲区写入数据写入的偏移量
length 需要读取的字节数
position 文件读取的起始位置
callback 回调函数
err 返回错误信息
bytesRead 所读取的字节数
buffer 缓冲区对象

.fs.read() 可用于更加合理的文件操作,这其中主要包括了 文件打开 -> 文件写入 —> 文件读取 -> 关闭文件 的整一套流程,让整个对文件的操作更加合理和直观

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const fs = require('fs')
const buf = new Buffer.alloc(1024)

/*
准备打开文件
文件打开成功
开始准备读取文件
文件内容为: Hello, File system
文件关闭成功
*/

console.log("准备打开文件")

fs.open('data.txt', 'r+', function (err,fd) {
if (err) {
console.log(err)
}
console.log('文件打开成功\n开始准备读取文件')
fs.read(fd, buf, 0, buf.length, 0, function (err, bytesRead) {
if (err) {
console.log(err)
}

if (bytesRead>0) {
console.log('文件内容为:',buf.slice(0,bytesRead).toString())
}

fs.close(fd, function (err) {
if (err) {
console.log(err)
}
console.log('文件关闭成功')
})
})
})

fs.open()

Id Name Info Type
1 fs.open(path[, flags[, mode]], callback) 异步打开文件 底层文件操作
path 文件路径
flags 文件的打开行为
a 如果文件不存在则创建文件
ax a 的基础上,如果文件路径不存在,就失败
a+ 如果文件不存在就创建
ax+ a+ 的基础上,如果文件路径不存在,则读取失败
r 以读取的方式打开文件,如果文件不存在则异常
r+ 以读写模式打开文件,如果文件不存在就抛出异常
rs 以同步的方式读取文件
rs+ 以同步的方式读取和写入文件
w 以写入模式打开文件,文件不存在则创建
wx w 的基础上,如果文件路径存在,文件将会写入失败
w+ 以读写模式打开文件,如果文件不存在则创建
wx+ w+ 的基础上,如果文件路径存在,则文件写入失败
1
2
3
4
5
6
7
8
9
const fs = require('fs')

// 文件打开成功
fs.open('data.txt', 'r+', function (err, fd) {
if (err) {
return console.error(err)
}
console.log("文件打开成功")
})

fs.writeFile()

Id Name Info Type
1 fs.writeFile(file, data[, options], callback) 通过异步的方式写入文件 文件内容读写
file 文件名
data 需要写入的数据
options 一个参数对象,包含了编码模式(utf-8)以及 flags(w,写入模式打开文件,文件不存在则创建)
1
2
3
4
5
6
7
8
9
// 写入成功: Hello, File system
fs.writeFile('data.txt', data, function (err) {
if (err) {
console.log(err)
} else {
var write = fs.readFileSync('data.txt', 'utf-8')
console.log('写入成功:' ,write)
}
})

fs.writeFileSync()

.fs.writeFileSync()fs.writeFile() 的同步方法,同样也可以使用 try...catch 来支持返回 err 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require('fs')

var data = "Hello, File system"

fs.open('data.txt','r+', function (err,fd) {
if (err) {
return console.error(err)
} else {
fs.writeFileSync('data.txt', data)
// 写入成功 Hello, File system
console.log('写入成功', data )
}
})

获取文件属性 (fs.stat())

“获取文件属性”是指文件的大小、创建时间、修改时间等信息,可以通过 fs.stat(path[, options], callback) 来进行实现,在这个方法中存在着 fs.stats 类,用于获取文件的属性,常用的为:

Id Name Info
1 stat.isFile() 是否是文件
2 stat.isDirectory() 是否是目录
3 stat.size() 文件大小(以字节为单位)
4 stat.birthtime() 创建时间
5 stat.mtime() 修改时间
6 stat.ctime() 最后一次更改时间
7 stat.atimeMS() 最后一次访问时间
8 stat.mtimeMS() 最后一次修改时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require('fs')

/*
是否是文件: true
文件大小: 18
创建时间: 2021-09-01T02:23:04.637Z
修改时间: 2021-09-01T02:23:04.637Z
最后访问: 2021-09-01T02:23:04.949Z
*/
fs.stat('data.txt', function (err, stat) {
if (err) {
console.log(err)
} else {
console.log("是否是文件:", stat.isFile())
if (stat.isFile()) {
console.log('文件大小:', stat.size)
console.log('创建时间:', stat.birthtime)
console.log('修改时间:', stat.mtime) // stat.ctime
console.log('最后一次访问时间:', stat.atime)
}
}
})

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js Buffer

Buffer

缓冲区是内存区域,和直接与内存交互的编程语言 C、C++、Go 等相比,JavaScript 对这个操作很微乎其微,因此为了实现与内存区域打交道,Buffer 由此而生。

Buffer class

Buffer 类主要的作用就是一个专门用于存放二进制数据的缓存区,是 Node 专门定义的一个类,用于弥补 JavaScript 自由的字符串类型无法满足大量二进制数据处理。

Buffer 是 Node 的一个核心库,因此为 Node 带来了另一种原始的数据存储方法(二进制),让 Node 处理 二进制数据。

所以 Buffer 库主要的用处就是需要 Node 处理 I/O 的时候,就可以通过 Buffer 库进行一个处理,当然也可以进行字符编码或数据类型的转换。

缓冲区可以通过使用 Buffer.[form(),alloc(),allocUnsafe()] 方法进行构建,这也涉及到了 Buffer 方法:

Id Name Info Type
1 buffer.alloc 可以创建任意大小的缓存区 type
2 buffer.from(arrayBuffer[, byteOffset[, length]) 能够通过字符串、缓冲区、数组等方式创建缓存区 type
byteOffset 需要暴露的地一个索引值
length 暴露的字节数
0x1 buffer.byteLength() 查询字节长度 method
0x2 buffer.compare(target[,target1,target..]) 比较两个缓冲区(buffer)对象是否相等,并返回 -1、0、1 method
0 与 target 相等返回 0
1 如果 target 的 size 不同以及 fill 相等,则返回 1
-1 如果 target 的 fill 不相等,则返回 -1
0x3 buffer.concat 将一个或多个缓存区对象合并为一个对象,合并后可以获取新的对象长度 method
0x4 buffer.entries() 用于返回缓冲区的 [index,byte] method
0x5 buffer.fill() 用于缓冲区填充 method
0x6 buffer.includes(values) 用于判断 values 是否在缓冲区(buffer)中 method
0x7 buffer.isEncoding 判断 Buffer 目前所支持的编码名称 method
0x8 buf.slice(start[,end]) 用于将 buffe 内的 fill 进行切片 method
0x9 buf.toJSON() 将数据输出为 JSON 格式 method
0x10 buf.write(string[,offset,length,encoding]) 将 string 写入 buffer 中 method

buffer.alloc

1
2
3
4
5
6
7
8
9
10
const buf = Buffer.alloc(10, 1)

// <Buffer 01 01 01 01 01 01 01 01 01 01>
console.log(buf)

// 返回字节的长度
var buflen = Buffer.byteLength(buf)

// 10
console.log(buflen)

创建一个大小为 10 的缓存区,且用 1 进行填充,并通过 Buffer.byteLength 来查询字节的长度。

length 的区别是 byteLength 不会考虑用于将字符串转换为字节编码

buffer.fill(arrayBuffer[, byteOffset[, length])

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过 Unit8Array 创建一个数组
const arrA = Uint8Array.from([0x61, 0x62, 0x63, 0x64])

const arrB = new Uint8Array(arrA.buffer, 0,3)

const buf = Buffer.from(arrB.buffer)

// abcd
console.log(buf.toString())

// or
const buff = Buffer.from([0x61, 0x62, 0x63, 0x64])
console.log(buff.toString())
buf.toJSON()
1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过 Unit8Array 创建一个数组
const arrA = Uint8Array.from([0x61, 0x62, 0x63, 0x64])

const arrB = new Uint8Array(arrA.buffer, 0,3)

const buf = Buffer.from(arrB.buffer)

// { type: 'Buffer', data: [ 97, 98, 99, 100 ] }
console.log(buf.toJSON())

// or
const buff = Buffer.from([0x61, 0x62, 0x63, 0x64])
console.log(buff.toJSON())

buffer.compare(target[,target1,target…])

Id Name Info
0x2 buffer.compare(target[,target1,target..]) 比较两个缓冲区(buffer)对象是否相等,并返回 -1、0、1
0 与 target 相等返回 0
1 如果 target 的 size 不同以及 fill 相等,则返回 1
-1 如果 target 的 fill 不相等,则返回 -1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const buf1 = Buffer.alloc(10,1)
const buf2 = Buffer.alloc(10,1)

// 0
var com = Buffer.compare(buf1,buf2)
console.log(com)

const buf3 = Buffer.alloc(10)
const buf4 = Buffer.alloc(10,1)

// -1
var com1 = Buffer.compare(buf3,buf4)
console.log(com1)

const buf5 = Buffer.alloc(10,1)
const buf6 = Buffer.alloc(10,1)

// 1
var com2 = Buffer.compare(buf5,buf6)
console.log(com2)

buffer.concat()

1
2
3
4
5
6
7
8
9
10
const buf = Buffer.alloc(3)
const buf1 = Buffer.alloc(1)
const buf2 = Buffer.alloc(1)

var arr = [buf, buf1, buf2]

var con = Buffer.concat(arr)

// 5 <Buffer 00 00 00 00 00>
console.log(Buffer.byteLength(con), con)

buffer.entries()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const buf = Buffer.from('hello,world!')

/*
[ 0, 104 ]
[ 1, 101 ]
[ 2, 108 ]
[ 3, 108 ]
[ 4, 111 ]
[ 5, 44 ]
[ 6, 119 ]
[ 7, 111 ]
[ 8, 114 ]
[ 9, 108 ]
[ 10, 100 ]
[ 11, 33 ]
*/
for (var point of buf.entries()) {
console.log(point)
}

// 104
console.log(buf[0])

.buffer.entries() 与直接通过 buf[0] 这种逐一输出更加的方便和快捷。

buffer.fill

1
2
3
4
5
6
7
8
const buf = Buffer.alloc(5).fill('hello')

// hello
console.log(buf.toString())

// or
const buf2 = Buffer.alloc(5,'hello')
console.log(buf2.toString())

buffer.includes(values)

1
2
3
4
5

const buf = Buffer.from('This is Buffer')

// true false
console.log(buf.includes('is'), buf.includes('as'))

buf.slice(start[,end])

1
2
3
4
5
const buf = Buffer.from('abcd')
const sli = buf.slice(0, 2)

// ab
console.log(sli.toString())

buf.write(string[,offset,length,encoding])

1
2
3
4
const buf = Buffer.alloc(6);
const len = buf.write('buffer',0,6, 'utf8')

console.log(`byte: ${buf} length: ${len}`)

字符编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const buffer = Buffer.from('hello,world!', 'ascii')

// hello,world!
console.log(buffer.toString('ascii'))

/**
* 104 101 108 108
* h -> 104
* e -> 101
* l -> 108
* l -> 108
*/
console.log(buffer[0],buffer[1],buffer[2],buffer[3])

缓冲区是一个字节数据,因此可以像数组一样进行访问,除此它还支持数种编码的转换:

Id Name Info Type
1 uft8 (utf-8) 多字节的 Unicode 字符,许多网页和玩文档格式使用 UFT-8 也是 默认的字符编码(当 buffer 解码过程中如果 UTF-8 不是有效字符,将会以 unicode 替换字符 � 用于表示错误) 字符串
2 utf16le(utf-16le) 多字节编码的 Unicode 字符,与 utf8 不同的是字符串中每个字符都将使用 2个或4个 字节进行编码 字符串
3 latin1 (Latin-1) 即 ISO-8859-1 ,因此此编码仅支持 U+0000~U+00FF 的 Unicode 字符,每个字符都将使用单字节进行编码(一种一字节的字符串方式) 字符串
4 base64 一种基于 64 个可打印字符来表示二进制数据的方法,可打印的字母 a-z\A-Z 以及数字 0-9,此外还包含空白字符(空格、制表符或换行符)会被 buffer 忽略 二进制
5 base64url 一个对 base64 标准的修改,其目的是能将编码结果用于文件名或 URL 地址,当字符串通过 buffer 时,base64url 也将接受常规的 base64 编码转换。 二进制
6 hex(16 进制) 将每个字节转换为十六进制字符 二进制
7 ascii 仅用于 7 位数的 ASCII 数据,当通过 buffer 时,将会等效于 Lantin-1 旧字符
8 binary (Lation-1) Lation-1 的别名,通过此编码列出的所有编码都会在字符串和二进制之间转换 旧字符
9 ucs2(utf-16le) uft-16le 的别名,泛指 UTF-16 的一种变体,此编码不支持大于 U+FFFF 的字符 旧字符

可以通过使用 buffer.isEncoding 方法来进行判断 Buffer 是否支持该编码类型。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js Model 引入流程

Node model 可以相互进行调用,Node.js 为此提供了一个简单的模块系统,这可以内置的也可以是通过 npm 进行安装的,因此 Node.js 提供了 exportsrequire 两个对象,其中 exports 用于模块公开的接口,而 require 则用于获取模块的 exports 对象:

1
2
3
4
5
6
7
8
9
10
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hey,' + name);
}
}
module.exports = Hello;

通过 exports 对象将 Hello 对象本身,因此在通过 require 对象进行引入的时候,就会输出 Hello 函数。

1
2
3
4
var Hello = require('./modelName')
hello = new Hello()
hello.setName('This is vievs file!')
hello.sayHello()

在 Node.js 模块中,模块主要分为 核心模块,这是 Node.js 所提供的模块,而另一个则是用户所编写的模块,被称之为 文件模块。这之间的区别就是核心模块部分在 Node 编译的过程中,同时编译了二进制的执行文件,所以在 Node 进程启动时,核心模块就会被加载到内存中,因此和因模块在引入的时候,文件定位以及编译执行就会被省略掉。

但用户所编写的文件模块则需要通过 查找路径,文件定位,编译执行 这三个流程,并在运行时进行动态加载,因此比较慢。所以和核心模块比起来,速度是自然赶不上人家内置的。

require 文件查找策略

文件缓存区

对于 require 的文件缓存区主要的作用就是会优先从文件模块中加载已经存在的模块这也就说明了 Node 对引入过的模块都会进行缓存,以减少第二次使用时的加载时间。

但与浏览器缓存静态脚本文件不同的是,Node.js 所缓存的是编译之后的对象,无论是核心模块还是文件模块,对于二次加载都一律采用缓存优先的方式进行。

不同之处在于核心模块的检查优于文件模块的缓存检查

原生模块加载

原生模块的加载仅次于检查文件缓存区是否存在,因此通过 require 解析完文件名之后,检查该模块是否在原生模块中。

http 模块,虽然会在项目目录下存在 http/http.js/http.node/http.json 文件,但 http 模块不会从这些文件中加载,而是从原生模块中进行加载。

因此原生模块内同样有也有一个缓存区,如缓存区没有被加载过,就会调用原生模块的加载方式进行加载和执行。

文件加载

当文件模块在缓存中并不存在是,而且还是原生模块的时候,require 方法所传入的参数就会被 Node.js 解析,并从文件系统中加载到实际的文件。对于没一个被加载的文件模块,创建这个模块对象的时候就会生成一个 paths 属性,也就是 /node_modules,因此我们可以直接通过 console.log(module.paths) 来根据当前文件的路径计算得到 module path

1
2
3
4
5
6
7
[ '/home/kunlun/Development/Web/Demo/node/node_modules',
'/home/kunlun/Development/Web/Demo/node_modules',
'/home/kunlun/Development/Web/node_modules',
'/home/kunlun/Development/node_modules',
'/home/kunlun/node_modules',
'/home/node_modules',
'/node_modules' ]

从当前文件目录内开始查找 node_modules 目录,然后依次进入查找父目录下的 node_modules,以此循环迭代到根目录下的 node_modules 目录。

如果 require 指向的是绝对路径的文件,查找时就直接作为当前的路径进行使用,如果不是则进入查找的流程。以 module_path 中取出地一个目录作为查找的基准:

  1. 直接尝试从当前目录进行查找,如果存在则结束流程,反而继续进行查找,并进入下一阶段
  2. 尝试添加扩展名后缀进行查找,如果存在就结束查找,否则进入下一个阶段
  3. 尝试将 require 的参数作为一个包进行查找,读取项目目录下的 package.json 文件
  4. 尝试添加扩展名或查找文件,如果存在就结束流程,否则进入下一个阶段
  5. 如果继续失败,则取出 module path 中下一个目录作为查找的基准,循环 1~5 步骤
  6. 如果仍然失败,则一直循环 1~6 步骤,知道 module path 中最后一个值
  7. 如果持续失败,则抛出异常。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Node.js at ECMAScript

首先我们需要知道 ECMAScript 是一种由 Ecma 国际(Ecma International)根据其标准定义的脚本语言规范,主要在 JavaScript 上运用广泛,而 ECMAScript 是在 2015 年 6 月所正式发布,这使得 JavaScript 可以用来编写复杂的大型应用程序,成为企业级开发语言。

ECMAScript 前身为欧洲计算机制造商协会(European Computer Manufacturers Association,ECMA),是一个国际性会员制度的信息和电信标准组织。

因此遵循 ES6 中的 JS 语法也是 Node.js 基础之一,因为 Node.js 本质上还是 JavaScript 所以通过了解并学习 ES6 中的方法可以快速掌握 JS。

数组

Id Name Info
1 array.push() 将一个或多个元素添加到数组的末尾,并返回数组的长度
2 array.pop() 删除最后一个访问最后一个数组
3 array.shift() 删除第一个数组,并返回删除后的第一个数组
4 arry.shift() 从数组起始位置添加元素,并返回添加后的长度
5 array.splice(start,deleteCount) 根据数组的索引删除数组,并返回删除的元素
6 array.concat() 将数组合并
7 array.split()) 用于将字符串形式转为数组
8 array.sort() 通过原地算法对数组进行排序,返回数组(根据 UTF-16 代码值进行排序)
9 array.reverse() 将方法数组中的元素颠倒,并返回数组,之后原本第一个数组将会变成最后一个
10 array.slice(start, end) 删除索引值 start ~ end 的数组(从 0 开始)
11 array.forEach(callback(currentValue [, index [, array]]) 用于便利数组,并未每个元素执行一次函数( array.map 的区别就是不返回数组 ),该函数主要分为三个 :
index 索引号
value 元素
array 数组
12 array.map(callback(currentValue [, index [, array]]) 用于便利数组,并未每个元素执行一次函数,并返回新的数组,该函数主要分为三个 :
index 索引号
value 元素
array 数组
13 array.filter() 返回满足需要的新数组
14 array.every() 判断数组元素是否满足,并返回布尔值
15 array.some() 一句条件判断数组中元素是否满足,并返回布尔值(数组内满足一向即可)
16 arr.reduce(callback, currentValue[, index[, array]])[, initialValue]) 通过定义的 reducer 函数来返回一个最终值
initialValue 执行数组中的每个值,如果提供了,则 previousValue(callback) 成为第一个值,等于 initialValue
currentValue 等于数组中的第一个值
17 Array.from() 将一个类似的数组转换为数组
18 Array.of() 将一组值转换为数组
19 array.copyWithin(target, start, end) 用于将当前位置的数组元素复制到目标位置并覆盖原来的元素
target 为目标位置,从 0 开始
start 是当前元素最初位置
end 当前元素结束的位置
20 array.find(index, value, array) 用于遍历数组,并根据表达式找出符合条件的数组成员,该函数主要分为三个 :
index 索引号
value 元素
array 数组
21 array.findIndex(index, value, array) 用于遍历数组,并根据表达式找出符合条件的数组成员( array.find() 区别在于如果未找到符合要求的则返回 “-1” ) ,该函数主要分为三个:
index 索引号
value 元素
array 数组
22 array.fill(value, start, end) 通过索引来添加元素 ,该函数主要分为三个:
value 待添加元素
start 开始位置索引
end 最终位置索引
23 array.includes(Value) 判断数组中是否包含 Value,并返回布尔值
24 array.keys() 遍历数组键名
25 array.values() 遍历数组键值
26 array.entries() 遍历数组的键名和键值

array.push

1
2
3
4
5
6
7
8
9
10
const arr = ['One','Two','Three']

// 3
console.log(arr.push())

// To arr add Four
arr.push('Four')

// [ 'One', 'Two', 'Three', 'Four' ]
console.log(arr)

array.pop

1
2
3
4
5
6
7
8
9
10
const arr = ['One','Two','Three']

// [ 'One', 'Two', 'Three' ]
console.log(arr)

// 删除最后一个元素
arr.pop()

// 访问最后一个元素:Two
console.log(arr + ' 当前数组最后一个元素是:' + arr.pop())

array.shift

1
2
3
4
5
6
7
8
9
10
const arr = ['One','Two','Three']

// [ 'One', 'Two', 'Three' ]
console.log(arr)

// 删除 One
arr.shift()

// 第一个元素为 Two
console.log(arr + ' 当前数组第一个元素是:' + arr.shift())

array.unshift

1
2
3
4
5
6
7
const arr = ['One','Two','Three']

// 4
console.log(arr.unshift('Start'))

// [ 'Start', 'One', 'Two', 'Three' ]
console.log(arr)

array.splice(start,deleteCount)

1
2
3
4
5
6
7
const arr = ['One','Two','Three']

// [ 'Two', 'Three' ]
console.log(arr.splice('1','3'))

// [ 'One' ]
console.log(arr)

array.concat

1
2
3
4
5
6
7
8
9
10
const arr = ['One','Two','Three']

// 新增 Four,Five
const arr__ = ['Four','Five']

// 合并两个数组
const __arr = arr.concat(arr__)

// [ 'One', 'Two', 'Three', 'Four', 'Five' ]
console.log(__arr)

array.split

1
2
3
4
const arr = 'Hello,world!'

// [ 'Hello,world!' ]
console.log(arr.split())

array.sort()

采用了原地算法(in-place algorithm),即不需要借助额外的数据结构就可以对输入的数据进行变幻,而 array.sort() 是根据 UTF-16 代码单元值序列进行构建的,因此还是取决与代码值的顺序,无法保证排序时间和空间的复杂性。

1
2
3
4
5
6
7
8
9
10
const arr = ['1','3','2','4','5','6']

// 默认情况下是从小到大进行排序
console.log(arr.sort())

// 将排序顺序改为从大到小
const arr__ = arr.sort((a,b) => b-a);

// [ '6', '5', '4', '3', '2', '1' ]
console.log(arr)

array.reverse

1
2
3
4
5
6
7
8
9
10
const arr = ['One','Two','Three','Fire','Five']

// Array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
console.log('Array: ', arr)

// 将数组反转
const reversed = arr.reverse()

// Reverse: [ 'Five', 'Fire', 'Three', 'Two', 'One' ]
console.log('Reverse: ', reversed)

array.forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const arr = ['One','Two','Three','Fire','Five']
const arr_ = ['One','Two','Three','Fire','Five']

/*
index:0 value:One array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:1 value:Two array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:2 value:Three array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:3 value:Fire array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:4 value:Five array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
*/
arr.forEach((value, index, array) => {
console.log(`index:${index} value:${value} array:`, array)
})

/*
index:0 value:One array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:2 value:Two array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:4 value:Three array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:6 value:Fire array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
index:8 value:Five array: [ 'One', 'Two', 'Three', 'Fire', 'Five' ]
*/
arr_.forEach((value, index, array) => {
index = index * 2
console.log(`index:${index} value:${value} array:`, array)
})

array.map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const arr = ['One','Two','Three','Fire','Five']
const arr_ = [1,2,3,4,5,6]

/*
index:0 value:1 array: NaN
index:1 value:2 array: NaN
index:2 value:3 array: NaN
index:3 value:4 array: NaN
index:4 value:5 array: NaN
index:5 value:6 array: NaN
undefined
*/
const foreach = arr_.forEach((value, index, array) => {
array = array * 2
console.log(`index:${index} value:${value} array:`, array)
})

console.log(foreach)

/*
index:0 value:1 array: NaN
index:1 value:2 array: NaN
index:2 value:3 array: NaN
index:3 value:4 array: NaN
index:4 value:5 array: NaN
index:5 value:6 array: NaN
[ undefined, undefined, undefined, undefined, undefined, undefined ]
*/
const map = arr_.map((value, index, array) => {
array = array * 2
console.log(`index:${index} value:${value} array:`, array)
})

console.log(map)

array.filter

1
2
3
4
5
6
7
8
9
10
11
12
const arr = ['One','Two','Three','Fire','Five']
const arr_ = [1,2,3,4,5,6]

// [ 1, 2, 3 ]
let starr = arr_.filter((i,v) => i <= 3)

console.log(starr)

// [ 'One' ]
let starr_ = arr.filter((i,v) => v <= 0)

console.log(starr_)

array.slice(start,end)

1
2
3
4
5
6
7
const arr = ['One','Two','Three','Fire','Five']

// [ 'Three', 'Fire', 'Five' ]
console.log(arr.slice(2))

// [ 'One', 'Two', 'Three' ]
console.log(arr.slice(0,-2))

array.every(i,v)

1
2
3
4
5
6
const arr = ['One','Two','Three','Fire','Five']

// true
let ifarr = arr.every((i,v) => v < 10)

console.log(ifarr)

array.some(i,v)

1
2
3
4
5
6
7
8
9
10
11
const arr = ['One','Two','Three','Fire','Five']

// false
let ifarr = arr.some((i,v) => i < 30)

console.log(ifarr)

const arr_ = [1,2,3,4,5,6,7,8,9,10]

// true
console.log(arr_.some(arr_ => arr_ >= 10 ))

array.reduce(previousValue, currentValue)

1
2
3
4
5
6
7
8
const arr = [1,2,3,4,5]

// 20
let arr_ = arr.reduce((previousValue, currentValue) =>
previousValue + currentValue,5
)

console.log(arr_)

Array

Array.from()

1
2
3
4
const arr = 'hey'

// [ 'h', 'e', 'y' ]
console.log(Array.from(arr))

Array.of()

1
2
3
4
const arr = 'hey'

// [ 'hey' ]
console.log(Array.of(arr))

array.copyWithin(target, start, end)

1
2
3
4
5
6
7
8
9
const arr = ['One','Two','Three','Fire','Five']

/** 用于将当前位置的元素复制到目标位置,并覆盖原来的元素
* target 为目标位置(0为开始) ·「1」
* start 是最初的位置 ·「0」
* end 是结束的位置 ·「1」
* [ 'One', 'One', 'Three', 'Fire', 'Five' ]
*/
console.log(arr.copyWithin(1,0,1))

array.find(index, value, array)

1
2
3
4
5
6
7
8
9
10
11
const arr = ['One','Two','Three','Fire','Five']

/** 根据表达式找出符合条件的数组成员
* @type {string}
* @private index 索引
* @private value 元素
* @private array 数组
* @return One
*/
let arr_ = arr.find((index, value, array) => index => 1)
console.log(arr_)

array.findIndex(index, value, array)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arr = ['One','Two','Three','Fire','Five']

/** 根据表达式找出符合条件的数组成员,如果未找符合要求的则返回 "-1"
* @type {number}
* @private index 索引
* @private value 元素
* @private array 数组
* @return -1
*/
let arr_ = arr.findIndex((index, value, array) => value > 199)
console.log(arr_)

/** 根据表达式找出符合条件的数组成员
* @type {string}
* @private index 索引
* @private value 元素
* @private array 数组
* @return undefined
*/
let arr_find = arr.find((index, value, array) => value > 199)
console.log(arr_find)

array.fill(value, start, end)

1
2
3
4
const arr = ['One','Two','Three','Fire','Five']

// [ 'Start', 'Two', 'Three', 'Fire', 'End' ] [ 'Start', 'Two', 'Three', 'Fire', 'End' ]
console.log(arr.fill('Start',0,1),arr.fill('End',4,5))

array.includes(Value)

1
2
3
4
5
6
7
const arr = ['One','Two','Three','Fire','Five']

/** 判断数组中是否包含 Value
* One: true
* Start: false
*/
console.log('One:',arr.includes('One'),'\nStart:', arr.includes('Start'))

for……in

array.keys()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = ['One','Two','Three','Fire','Five']

let toarr = arr.keys()

/** 遍历数组键名
0
1
2
3
4
*/
for (let keys of toarr) {
console.log(keys)
}

array.values()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = ['One','Two','Three','Fire','Five']

let toarr = arr.values()

/** 遍历数组键值
One
Two
Three
Fire
Five
*/
for (let values of toarr) {
console.log(values)
}

array.entries()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = ['One','Two','Three','Fire','Five']

let toarr = arr.entries()

/** 遍历数组键名和键值
[ 0, 'One' ]
[ 1, 'Two' ]
[ 2, 'Three' ]
[ 3, 'Fire' ]
[ 4, 'Five' ]
*/
for (let keys of toarr) {
console.log(keys)
}

函数

通过 ECMAScript6 函数带来了很多扩展以及新的特性,这其中包括了新扩展的 REST 参数、解构参数、扩展运算符、箭头函数等,以及 NAME 属性。

参数默认值

ES6 允许作为函数的参数设置默认值,直接写才参数后面即可,而在此之前则需要加上参数的判断以及空字符的问题。

1
2
3
4
5
6
7
8
9
10
11
12
function log(x,y = 'world!') {
console.log(x,y)
}

/*
Hello world!
Hello ,world!
Hello
*/
log('Hello')
log('Hello',',world!')
log('Hello','');

除了简介的写法,我们还要注意的是,函数中生命的参数变量,都是 默认声明 的,因此通过 let/const 再次声明就会出现报错的问题:

1
2
3
4
function err (x = 1,y = 1) {
let x = 1; // error
let y = 2; // error
}

还有一个问题就是,只有在传递参数为 undefined 是,才会使用默认参数,而 null 、和空字符将会被认为有值传递

1
2
3
4
5
6
7
8
9
10
function na(x,y = 18) {
console.log(x+',',y)
}

/*
User, null
User,
*/
na('User',null)
na('User','')

解构赋值

解构赋值可以与参数默认值结合起来使用,通过解构赋值可以将值从对象中取出,赋值给其他的变量。

1
2
3
4
5
6
7
8
9
10
11
12
function na({x,y = 18}) {
console.log(x,y)
}

/*
undefined 18
1 18
1 1
*/
na({})
na({x:1})
na({x:1, y:1})

上述的 code 使用了对象的解构负值的默认值,只有当函数 na 的参数是一个对象时,变量 xy 才会通过解构赋值生成。

在下述的 code 中,两种写法分别都对函数的参数默认值进行了设定,one 的函数参数默认值是空对象,但是设置了对象解构默认值。而 two 则是有一个具体的属性的对象,但是没有设置对象解构的默认值,因此两者在输出时的差异如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function one({x = 0, y = 0} = {}) {
console.log([x,y])
}

function two({x,y} = {x:0, y:0}) {
console.log([x,y])
}

/*
[ 0, 0 ]
[ 0, 0 ]
*/
one()
two()

/*
[ 1, 2 ]
[ 1, 2 ]
*/
one({x:1, y:2})
two({x:1, y:2})

/*
[ 1, 0 ]
[ 1, undefined ]
*/
one({x:1})
two({x:1})

/*
[ 0, 0 ]
[ undefined, undefined ]
*/
one({})
two({})

不定参数(剩余参数)

不定参数也被称之为 ”剩余参数“,他主要用于表示不确定参数的个数,由 ...变量名 进行定义,具名参数只能放在参数组的最后,并且只能有一个不定参数。

1
2
3
4
5
6
function foo(...values) {
console.log(values.length)
}

// 5
foo('one','two','three','fore','fire')
rest at arguments

这里的 rest 并不是已经口口相传的 REST API,而是休止符的意思:

1
2
3
4
5
6
7
8
9
10
function add(...values) {
let sum = 0

for (var val of values) {
sum += val;
}
console.log(sum)
}

add(2,5,3)

正因为通过不定参数的作用,因此可以利用其向函数内传入任意数值并通过求和函数进行计算,通过使用 rest 的方法可以代替 arguments 变量,而他主要是传递函数的是类似的数组对象,但通过 Array.prototype.slice.call 可以转换为一个真正的数组对象,这也是他与 rest 的区别, rest 参数就是一个真正的数组,数组所特有的方法都可以使用

name 属性

函数的 name 属性,是在 ES6、ES5 中可以获取和调用函数的一个方法,他在 ES6 中的表现非常的方便以及快捷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {}

// foo
console.log(foo.name)

// 匿名函数
var of = function () {}

// of
console.log(of.name)

// 将具名函数赋值给变量
var te = function st() {}

// st
console.log(te.name)

箭头函数

箭头函数主要使用到的还是一些运算符表达式中的几项组成,在函数中使用这些箭头函数可以显得更加简洁的函数书写方式。

1
2
3
4
5
6
7
8
9
function fn() {
setTimeout(() => {
console.log(this.a + 10);
},0)
}
var a = 10

// 20
fn.call({a});

需要注意的是,箭头函数内是不会包含 this 的,因此上述箭头函数体中的 this 对象是定义函数时的对象,而不是使用函数时的对象。

闭包

闭包(Closure)简单来说就是能够读取外部函数的变量,这个概念最早出现在 60 年代,其最早实现闭包的程序语言是 Scheme,之后开始被广泛的运用在了函数式编程语言之中。简单理解就是,当每创建一个函数时,闭包就会在函数创建的同时被创建出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 在 for 循环内部中定义了内部函数 i,并通过内部循环数组 push x5
* 因此内部函数可以获取到每次循环的值,执行了 close 函数并最后赋
* 值给了 test。
* @return {[i:5]}
*/
var close = function () {
var arr = [];
for(var i=0; i<5; i++) {
arr.push(function () {
return i * i;
})
}
return arr;
}

var test = close();

/* 开始通过 test 来调用函数,开始执行 close 内的 for 循环
25
25
25
*/
console.log(test[0]())
console.log(test[0]())
console.log(test[0]())

之后我们换一个写法,当 for 循环每次执行时,i 会作为参数实时传输到内部函数中(a),因此获取的实际上是数组,调用时通过 test 来调用函数,开始执行 close 内的 for 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 当 for 循环每次执行时,i 会作为参数实时传输到内部函数中(a),因此获取的实际上是数组
* @return {[ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ]}
*/
var close = function () {
var arr = [];
for(var i=0; i<10; i++) {
arr.push(function (a) {
return a * a;
}(i))
}
return arr;
}

var test = close();

/* 开始通过 test 来调用函数,开始执行 close 内的 for 循环
[ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ]
*/
console.log(test)

从上述的 code 中,我们将外层的匿名函数(close)的返回值也变成了一个函数,并让内层函数的返回值成为了我们需要的结果,但是由于内层函数保持着对 i 的引用

这也导致了 i 的值在内存中并没有被释放,这就让我们之后通过test 来调用函数,开始执行 close 内的 for 循环时,也能获取到 i 的值。

let at var 实现闭包

在 MDN 文档中,对 var 的描述是:”声明一个变量,并可选的将其初始化为一个值“,而后者则是:”声明一个块级作用域的本地变量、语句或表达式,同样可以初始化为一个值“
,与 var 声明语句的区别是,var 声明的变量只可以是全局或者是整个函数块的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var close = function () {
const arr = []
for (let i = 0; i < 5; i++) {
arr.push(function () {
return i*i;
})
}
return arr
}

var test = close();

/**
* 在输出时将会将 const arr = [] 和 let i = 0 替换,但由于 let 声明的因素(本地变量)
* 所以每次 for 的循环都会将i 直接固定下来而不会受到外部影响,实现了闭包的效果
*/
console.log(test[1]())
console.log(test[1]())
console.log(test[1]())

对象

属性的简写

基于 ES6 属性的扩展,因此可以直接写入变量和函数,作为对象的属性和方法(属性名就是变量名,属性值就是变量值)

1
2
3
4
5
var foo = 'bar'
var bar = {foo}

// { foo: 'bar' } 同等于 var bar = {foo: foo}
console.log(bar)

函数

还有一些比如我们认为非常正常的表达式,但是在 ES6 之前是非常繁琐的,来对比下简写和非简写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6 add
function a(x,y) {
return {x,y}
}

// { x: 1, y: 2 }
console.log(a(1,2))

// normal
function b(x,y) {
return{x:x, y:y}
}

// { x: 1, y: 2 }
console.log(b(1,2))

函数方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = {
method() {
return "hey,world!";
}
}

// hey,world!
console.log(a.method())


var b = {
method: function () {
return "hey,world!";
}
}

// hey,world!
console.log(b.method())

变量属性简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var a = ',world!'

var toa = {
name: 'hello',
hey() {
console.log('info:', this.name + a)
}
}

// info: hello,world!
toa.hey()


var b = ',world!'

var tob = {
name: 'hello',
hey: function () {
console.log('info:', this.name + b)
}
}

// info: hello,world!
tob.hey()
迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const a = {
* myGenerator() {
yield 'One'
yield 'Two'
yield 'Three'
yield 'Four'
yield 'Five'
}
};

/*
One
Two
Three
Four
Five
*/
let echoa = a.myGenerator()

console.log(echoa.next().value)
console.log(echoa.next().value)
console.log(echoa.next().value)
console.log(echoa.next().value)
console.log(echoa.next().value)

const b = {
myGenerators: function * () {
yield 'One'
yield 'Two'
yield 'Three'
yield 'Four'
yield 'Five'
}
};

/*
One
Two
Three
Four
Five
*/
let echob = b.myGenerators()

console.log(echob.next().value)
console.log(echob.next().value)
console.log(echob.next().value)
console.log(echob.next().value)
console.log(echob.next().value)

属性名表达式

属性名表达式是 JavaScript 所定义的属性,主要有两种方法进行定义其属性名(也就是将表达式放在 [] 内):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = {
foo: true,
boo: 1
}

// { foo: true, boo: 1 }
console.log(a)

let bkey = 'foo'

let b = {
[bkey]: true,
['b' + 'oo']: 1
}

// { foo: true, boo: 1 }
console.log(b)

需要注意的是,属性表达式不可以被简写,否则将会报错,还需要留意的是,如果属性表达式是对象,那么将会被转换为字符串 [object Object] ,并只显示最后一个属性,因为前几个都被 KeyC 覆盖掉了

1
2
3
4
5
6
7
8
9
10
11
12
const keyA = {a:1}
const keyB = {b:2}
const keyC = {b:3}

const myObject = {
[keyA]: 'ValueOne',
[keyB]: 'ValueTwo',
[keyC]: 'ValueThree'
}

// { '[object Object]': 'ValueThree' }
console.log(myObject);

方法的 name 属性

方法的 name 属性,是在 ES6、ES5 中可以获取和调用函数的一个方法,他在 ES6 中的表现非常的方便以及快捷,他同样还提供了函数的 name 属性。

1
2
3
4
5
6
7
8
var log = {
myName() {
console.log(this.name)
},
};

// myName
console.log(log.myName.name)

当然还有一些特殊的方法 bind() 使用此方法的函数,返回时将会连带 bound + 函数名 进行输出,需要注意的是 bind() 方法是可以对其绑定的:

1
2
3
4
var doSomething = function () {}

// bound doSomething
console.log(doSomething.bind().name)

Symbol at name

如果对象的方法是一个 symbol 数据类型的值,那么通过 name 书香将返回的是这个属性值的描述:

.Symbol 是一个基本的数据类型(Primitive data type),其中每个值都是唯一的,因此一个 symbol 值可以具有作为对象属性的标识符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const keyA = Symbol('one')
const keyB = Symbol('two')

let obj = {
[keyA](){},
[keyB](){}
};

/*
[one]
[two]
*/
console.log(obj[keyA].name)
console.log(obj[keyB].name)

可参考其文档: https://www.typescriptlang.org/docs/handbook/symbols.html

对象扩展运算符

解构赋值 (rest)

对象的扩展运算符就是将解构赋值(REST)和扩展运算符(…)引入对象,这种操作在 Bable 编译器中已经支持这项功能。

Babel.js 是一个用于 Web 开发,且自由开源的 JavaScript 编译器以及转译器。

对象的解构赋值主要是用于从对象内进行取值,用于将所有可遍历但尚未完全读取的属性分配到指定的对象上,所有键值和他们的值都会拷贝到新的对象中。

1
2
3
4
5
6
7
8
9
10
let  {x, y, ...z} = {x:1, y:2, a:3, b:4}

// 1
console.log(x)

// 2
console.log(y)

// { a: 3, b: 4 }
console.log(z)

需要注意的是,因为解构赋值是 z,所以他获取等剩余的尚未被读取的对键,因此会将剩余的键全部输出(即: a at b)

因为解构赋值的写法,所以解构复制必须是最后一个参数,否则将会出错。

1
2
3
4
5
6
7
let obj = {a:{b:1}}

let {...x} = obj

obj.a.b = 5;

console.log(x.a.b)

在上述代码中,x 是解构赋值所在对象,引用了 obj 对象的 a 属性,而 a 属性同时引用了 b 属性,而之后还引用了 obj.a.b 属性,因此这个属性将会对解构赋值产生影响。

从而导致了最后输出时原本输出结果为 1,经过一系列引用输出结果被影响的问题,所以解构赋值不会拷贝继承自原型对象的属性(即 obj)

当然也可以通过一个更为简单的方法解决这个问题,从而避免了解构赋值被影响的问题:

1
2
3
4
5
6
7
8
let keyA = {a:1}
let keyB = {b:2}

let b3 = {...keyB}
let b4 = {...keyA}

// { b: 2 } { a: 1 }
console.log(b3,b4)

因为 b3\b4 都只是 keyA\keyB 的引用,因此也只是赋值了自身的属性,被没有可以被其他函数影响的余地。

扩展运算符 (…)

与 Object.assign 方法
1
2
3
4
5
6
7
let keyA = {name:'Aswl', age: '19',measurements: '100'}

// let getB = {...keyA}
let getB = Object.assign({},keyA)

// { name: 'Aswl', age: '19', measurements: '100' }
console.log(getB)

扩展运算符的的作用与 Object.assign 方法同等使用,不过扩展运算符的使用较为简单和便捷,他可以完成 Object.assign 方法无法完成的功能

合并对象
1
2
3
4
5
6
7
8
let keyA = {name:'Aswl', age: '19'}
let keyB = {measurements: '100'}

// let getB = {...keyA}
let getB = {...keyA, ...keyB}

// { name: 'Aswl', age: '19', measurements: '100' }
console.log(getB)

ES6 对象新方法

Id Name Info
1 Object.is() 用于判断两个对象值是否一样,同等于 ===
2 Object.assign() 将目标对象一个或多个分配到目标对象中,因此他也有合并对象的作用

Object.is

1
2
3
4
5
let keyA = '10'
let keyB = '10'

// true
console.log(Object.is(keyA,keyB));

通过 ES6 所提出的同值相等(Same-Value Equality)算法来解决严格比较运算符就基本行为一致,从而产出了 Object.is 方法,他与绝对运算符更让人容易理解,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function keyC () {
return NaN;
}
let keyA = null
let keyB = null

// true
console.log(+0 === -0)
// false
console.log(keyC === keyB)

// false
console.log(Object.is(+0,-0))
// true
console.log(Object.is(keyC,keyC))

在绝对运算符中,+0-0 被认为是一样的,而在此 ES6 新提出的算法时将不会被认为这是等同的,即 false。

而同样的还有 Nan 以及 Null 在绝对运算符中被认为是不等同的,而在 Same-Value Equality 算法下中是等同的。

Object.assign

1
2
3
4
5
6
7
8
var target = {a:1}
var source_1 = {b:2}
var source_2 = {c:3}

Object.assign(target,source_1,source_2)

// { a: 1, b: 2, c: 3 }
console.log(target)

虽然他具有合并对象的作用,但还是需要注意,如果目标对象之间有同名的属性,那么后面的属性就会被覆盖:

1
2
3
4
5
6
7
8
var target = {a:1}
var source_1 = {b:2}
var source_2 = {b:3}

Object.assign(target,source_1,source_2)

// { a: 1, b: 3 }
console.log(target)

除此之外还需要注意,如果 Object.assion 的参数不是对象,那么他也会直接返回该参数。

但如果对象如果是 undefinednull,因为他们无法转为对象,所以会报错,但是如果将他们放置在 source 参数中,当发现无法转换为对象时,就会被跳过:

1
2
3
4
5
6
7
8
var target = {a:undefined}
var source_1 = {b:null}
var source_2 = {b:3}

Object.assign(target,source_1,source_2)

// { a: undefined, b: 3 }
console.log(target)
复制与拷贝

至于其他的值,如果不在首参数中,也不会被报错之类的,除了字符串会以数组的形式拷贝到目标对象外,其他的都不会产生效果:

1
2
3
4
5
6
var source = {a:1}

var obj = Object.assign({}, source)

// { a: 1 }
console.log(obj)

在上面的代码中,除了数值和之前的布尔值以及数值会合并到对象中,而字符串将会产生包装的现象:

1
2
// [Boolean: true] [Number: 123] [String: 'abc']
console.log(Object(true), Object(123),Object('abc'))

因此布尔值、数值、字符串的类型分别会转成包装对象进行输出,也可以看到他们的原始值类型,这些属性是不会被 Object.assign 拷贝的,因为他拷贝的是可枚举属性值:

1
2
3
4
5
6
7
8
9
var c = Object.assign({b:'c'},
Object.defineProperty({}, 'invisible', {
enumerable:false,
value: 'hey'
})
)

// { b: 'c' }
console.log(c)

上述代码中因为 inisible 并不是可枚举类型的, 因此他没有被 assign 拷贝进去。

浅拷贝与深拷贝

浅拷贝与沈拷贝的问题在于,Object.assign 方法所实现的是浅拷贝,而不是深拷贝,也就是说如果 source 是一个对象,那么也会得到这个对象的引用:

1
2
3
4
5
6
7
var source = {a: {b:1}}
var get = Object.assign({}, source)

source.a.b = 10

// { a: { b: 10 } }
console.log(get)

因为源对象的 a 属性是一个对象,因此拷贝得到的是一个对象的引用,因此在输出的时候会被影响,对于这种方法,一旦遇到了同名的属性,将会直接替换。

1
2
3
4
5
6
7
var source = {a: {b:1}}
var sources = {a: {b:'hey'}}

var get = Object.assign({}, source,sources)

// { a: { b: 'hey' } }
console.log(get)

在上述的 code 中,因为 sourcea 属性被 sources 内的 a 给替换了,因此就会得到 a: { b: 'hey' } } 的结果。

对数组的处理

除了对对象的使用,Object.assign 还可以处理对象,但是会把数组视为对象。

1
2
// [ 4, 5, 3 ]
console.log(Object.assign([1,2,3],[4,5]))

因为 Object.assign0、1、2 视为对象,那么只好输出 target 部分的最后一个数组,以及在 source 位的 4,5,因此 target 数组的 0 号属性覆盖了属性 4 的属性,所以为 [4, 5, 3]

Object.keys()

用于遍历对象,并返回对象的键名,但在 ES7 的新提案中,提出了一种配套的方法 Object.keys()、Object.value()、Object.entries ,而原理都是循环遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let {keys, values, entries} = Object

let obj = {oneKey:"oneValue", twoKey:"twoValue", threeKey:"threeValue"}

/*
oneKey
twoKey
threeKey
*/
for (let key of keys(obj)) {
console.log(key)
}

/*
oneValue
twoValue
threeValue
*/
for (let value of values(obj)) {
console.log(value)
}

/*
[ 'oneKey', 'oneValue' ]
[ 'twoKey', 'twoValue' ]
[ 'threeKey', 'threeValue' ]
*/
for (let [key, value] of entries(obj)) {
console.log([key, value]);
}

对于 ES7新的提案中,就显得非常方便,只需要一个方法就能实现:

1
2
3
4
5

let obj = {key:"Value", keys:"Values"}

// [ 'key', 'keys' ]
console.log(Object.keys(obj))

Object.values()

1
2
3
4
let obj = {key:"Value", keys:"Values"}

// [ 'Value', 'Values' ]
console.log(Object.values(obj))

此方法只可以返回对象本身可以遍历对象,如果遇到数组的返回,那么返回的形式同样被打乱,索引为 0 的数将会被放到最后一个输出,而其他的索引则正常:

Object.entries()

1
2
3
4
let obj = {key:"Values", keys:"Values"}

// [ [ 'key', 'Values' ], [ 'keys', 'Values' ] ]
console.log(Object.entries(obj))

如果对象的属性是 Symbol 值的话,那么该属性就会选择忽略:

1
2
3
4
let obj = {key:"Values", keys:"Values", [Symbol()]: "null"}

// [ [ 'key', 'Values' ], [ 'keys', 'Values' ] ]
console.log(Object.entries(obj))

所以这个属性还衍生出了一种可以输出 Symbol 属性的 Reflect.ownEntries() 方法,返回对象自身的属性。而同时 Object.entries() 的主要作用就是返回对象的属性(单个)。

1
2
3
4
5
6
7
8
9
let obj = {key:"Values", keys:"Values", [Symbol()]: "null"}

/*
key:Values
keys:Values
*/
for (let [key, value] of Object.entries(obj)) {
console.log(`${key}:${value}`)
}

当然你还可以搭配 ${JSON.stringify()} 的配合让对象输出为 JSON 格式的数据。

除此之外他还可以通过配合 Map 方法来实现一个真正的 Map 数据结构:

1
2
3
4
5
6
7
8
let obj = {key:"Values", keys:"Values", [Symbol()]: "null"}

// Map { 'key' => 'Values', 'keys' => 'Values' }
var map = new Map(Object.entries(obj))
console.log(map)

// [ [ 'key', 'Values' ], [ 'keys', 'Values' ] ]
// console.log(Object.entries(obj))

Object.setPrototypeOf() at Object.getPrototypeOf and prototype

.Objcet.setPrototypeOf()Object.getPrototypeOf 以及 Object.create()三个对象是来自 __proto___ 属性,很明显是这个属性的代替,分别写操作和读操作

该属性是一个内置的属性,但由于 ES6 标准明文规定必须部署这个属性,导致他才加入了 ES6 :

在 ECMAScript 2015(ES6)的规范要求中,支持__proto__ 是各大Web浏览器厂商的要求(虽然符合规范),但其他环境下因为历史遗留的问题,也有可能被使用和支持。

同时 MDN 也建议不要使用这个属性:”该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。“

所以在这个环境下,Objcet.setPrototypeOf()Object.getPrototypeOf 以及 Object.create()三个对象就出现了,从而代替了 ````proto``` 属性。

prototype

Prototype 指的是 ”继承与原型链“,根据 MDN 的描述也非常简单粗暴直接点名重点:

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

如果你了解了这篇文章,那么 setPrototypeOfgetPrototypeOf 都会知道他们是干嘛的。

Object.setPrototypeOf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let proto = {};

let obj = {x:10}

/*
{ x: 10 }
20
40
10
20
40
*/
console.log(Object.setPrototypeOf(obj,proto));

console.log(proto.y = 20)
console.log(proto.z = 40)

// 因为 Object.setPrototypeOf 将 proto 对象的原型设置为 obj 所以可以读取
console.log(obj.x)
console.log(obj.y)
console.log(obj.z)

正因为 Object.setPrototypeOf 方法将指定一个对象(proto)到另一个域(obj) 所以可以读取。

Object.getPrototype() and Object.create

.Object.getPrototype() 属性用于返回指定对象的原属性用于返回指定对象的原型,也就是依赖的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let proto = {};

let obj = {x:10}
/*
{ x: 10 }
20
40
10
20
40
*/
console.log(Object.setPrototypeOf(obj,proto));

console.log(proto.y = 20)
console.log(proto.z = 40)

//
console.log(obj.x)
console.log(obj.y)
console.log(obj.z)

var let = '10';

// true
console.log(Object.getPrototypeOf(obj) === proto)

// false
console.log(Object.getPrototypeOf(let) === proto)

当然你还可以使用更加标准的方式,通过 Object.create() 的方法来创建一个新的对象,完美符合了 MDN 在 Prototype 上所说的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let proto = {};

let getS = {x:10}

// 创建一个新方法
let obj = Object.create(getS)

/*
{ x: 10 }
20
40
10
20
40
false
true
*/
console.log(Object.setPrototypeOf(getS,proto));

console.log(proto.y = 20)
console.log(proto.z = 40)

//
console.log(obj.x)
console.log(obj.y)
console.log(obj.z)

var let = '10';

// true
console.log(Object.getPrototypeOf(obj) === proto)

// false
console.log(Object.getPrototypeOf(getS) === proto)

当然,get.PrototypeOf 还可以读取 prototype 对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let proto = {};

let getS = {x:10}

// 创建一个新方法
let obj = Object.create(getS)
/*
{ x: 10 }
20
40
10
20
40
*/
console.log(Object.setPrototypeOf(obj,proto));

console.log(proto.y = 20)
console.log(proto.z = 40)

//
console.log(obj.x)
console.log(obj.y)
console.log(obj.z)

var let = '10';

// { y: 20, z: 40 }
console.log(Object.getPrototypeOf(obj))

属性的可枚举性(getOwnPropertyDescriptor)

在 ES6 中,每个属性都会有自己的一个描述对象(Descriptor)用于控制该属性的行为,那么通过 Object.getOwnPropertyDescriptor 就可以获取对象的描述对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let obj = {foo: 1234}

/*
value: 1234, 值
writable: true, 可写
enumerable: true, 可枚举
configurable: true 可配置
*/
console.log(Object.getOwnPropertyDescriptor(obj,'foo'))

/*
{ value: 'hey',
writable: false,
enumerable: false,
configurable: false }
*/
tes = {}
let sets = Object.defineProperty(tes,"inisible", {
value: 'hey',
writable: false,
enumerable:false
})

console.log(Object.getOwnPropertyDescriptor(sets,'inisible'))

在 ES5 中,当 enumerable 为 false 的属性会被忽略,其中主要有 :

Id Name Info
1 for...in 只循环遍历自身对象和继承的可枚举属性
2 Object.key 返回对象自身的所有可枚举的属性键名
3 JSON.stringify() 只串化对象自身的可枚举属性
4 Object.assign() 忽略自身为 enumerable 的属性
1
2
3
4
5
6
7
8
9
10
11
12
let obj = {foo: 1234}

console.log(Object.getOwnPropertyDescriptor(obj,'foo'))

let sets = Object.assign({b:'c'},
Object.defineProperty({},'inisible', {
enumerable:false,
value:'hey'
})
)

console.log(Object.getOwnPropertyDescriptor(sets,'inisible'))

在 Node.js 中,类是一个函数的集合体以及成员变量,但是他依然被认为称之为:“类(class)” 语法糖,他本质上让对象更加清晰。

定义

这是一个 ES6 中的语法糖,class 将会作为对象模板被引入,通过该关键字可以定义类,而他的本质就是 function ,只不过他可以让对象的原型更加清晰,更像面向对象的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 3
let example = class {
constructor(x,y) {
this.area = x + y;
}
}

console.log(new example(1,2).area)

// 3
class exmapleName {
constructor(x,y) {
this.area = x + y;
}
}

console.log(new exmapleName(1,2).area)

上述示例中主要分为匿名函数和命名函数,他们都实现了同样的效果和功能,但需要注意的是,不可以重复声明同一个类,如果声明将会抛出异常信息。

内部属性或静态属性

内部方法

在新的 ES6 规定中,class 内部只会有静态方法,不会有静态属性,而直接定义在类的内部属性(class.propname)将会被称之为静态属性,不需要实例化。

在 ES6 中 prototype 属性仍然存在,他本质还是定义在 prototype 上,且覆盖 prototype 属性,并在初始化时添加新的方法

1
2
3
4
5
6
7
8
class Example {
constructor() {
console.log('hello,world!')
}
}

// hello,world!
new Example()

需要注意的是,类的实例化必须是通过 new 关键字的

静态方法

类的 constructor 方法用于实例化对象时被调用,因此这个概念也被应用与大多数的面向对象编程语言,同时该方法下返回实例对象的 this.x 也可以指定返回的对象:

1
2
3
4
5
6
7
8
class Example {
static sum(x,y) {
console.log(x + y)
}
}

// 2
Example.sum(1,1)
decorators

装饰器 (decorators) 是ES7类的新提案,在转译器环境中被开发人员广泛的使用,实际上这是 ES5 所纳入的提案,但最这 ES6 中新引入的 class ,从而需要额外的功能来支持 注释或修改类和成员的场景

你可以可以在类、方法、属性、参数中进行声明,并使用 @expression 的形式调用函数。

你可以在 https://www.typescriptlang.org/docs/handbook/decorators.html#decorators 了解更多(ES5)

类的继承

extends

在类的内部中可以使用的 get 以及 set 关键字,对于某个睡醒内设置存值函数和取值函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class fatcher {
constructor() {}

// 绑定
get a() {
return this._a
}

// 调用
set a(a) {
this._a = a;
}
}

class child extends fatcher {
constructor() {
super();
}
}

let foo = new child();

foo.a = 10

// 10
console.log(foo.a)

class 可以通过 extends 关键字从而实现类的继承,并通过 getter/setter 分别对属性进行了绑定和调用,除此之外,child 通过 extends 继承了 fatcher ,所以可以被调用并输出数据,也就是说通过此关键字让两个类变得一样了。

super

super 关键字用于访问和调用对象中父对象上的函数,ES6 中要求子类和构造函数必须执行一次 super 函数,也就是说子类 constructor 方法中必须有 super。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class fatcher {
constructor() {}
static log() {
return 'Hey,'
}
}

class child extends fatcher {
static logDesc() {
return super.log() + 'world!';
}
}

// hey,world!
console.log(child.logDesc())

ES6 模块化

在 ES6 引入模块化之前,其模块化的使用是 RequireJS,用于 JavaScript 文件和模块加载,可以在浏览器环境中使用,可通过 Node 来实现模块化脚本加载来提高代码的编写质量和速度。

在 ES6 规范中引入的模块提案,其设计思想是在编译时候就能确定模块的依赖关系,以及输入和输出的变量,所以 ES6 中模块化主要分为导出(export)和导入(import)

添加 babel 支持

正因为使用的是 ES6,这在目前的版本中看样子很正常的语法但会因为一些语法糖的问题而出错,因此我们可以在 package.json 中以 babel 的转译器进行运行,并添加 ES6 支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"name": "node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"babel-preset-es2015": "^6.24.1",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.2.0"
},
"devDependencies": {
"@babel/cli": "^7.14.8",
"@babel/core": "^7.0.0-bridge.0",
"@babel/node": "^7.14.9",
"@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.15.0"
},
"scripts": {
"test": "babel-node vievs.js"
},
"assumptions": {
"setPublicClassFields": true
},
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
"transform-decorators-legacy",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties"
]
],
"author": "",
"license": "ISC"
}

这个 package.js 文件中主要用于安装 babel 的依赖,我们可以通过 npm init 来初始化一个,之后添加依赖即可,之后使用 npm install 等待安装完成即可。

安装后,我们需要新建一个 .babelrc 文件,添加一个 ES6 的支持:

1
2
3
{
"presets": ["es2015"]
}

export at import

1
2
3
4
5
6
let myName = "modelName";
let mySet = function () {
return "My in name:" + myName;
}

export {myName, mySet}

首先我们要创建一个文件,并写入方法,之后通过 export 来导出,最后在通过 ````import``` 进行引入:

1
2
3
4
import {myName, mySet} from "./modelName.js";

// Name: modelName My in name:modelName
console.log('Name:',myName, mySet())

我们可以直接通过 npm 运行:“npm run test”,也可以直接输入 bebel-node filename.js 运行。

as

.as 可以说是 export……import 所附带的一个方法,他主要的作用就是 可以建立 export 的连接,你可以理解为赋值:

1
2
3
4
5
6
let myName = "modelName";
let mySet = function () {
return "My in name:" + myName;
}

export {myName as exportName, mySet}
1
2
3
4
import {exportName, mySet} from "./modelName.js";

// Name: modelName My in name:modelName
console.log('Name:',exportName, mySet())

最后运行输出的还是与 exportName 建立链接的 myName,因此这也是 as 命令的作用。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

node.js 工作原理

Runtime system

运行时系统(Runtime system),又称之为 “运行环境”,指将半编译的代码运行的环境。在 node 核心概念中指的是 数据类型的确定 由编译时推迟到了运行时,因此就出现了运行时系统(Rutime System)来处理编译后的代码。

因此这个概念和流程就是 JavaScript 引擎负责解析和即时编译(JIT),结合 Rutime 因此可以实现了 Window 、DOM API,存在与浏览器的 Runtime 中。

所以 Node.js 中的 Runtime 也包含了不同的库,例如 Cluster、FileSystem API,这里面的 Runtime 都包含了内置的数据类型和常用的工具,因此 Chrome 和 Node.js 同样使用 V8 引擎但不会使用同一个 Runtime。

Cluster (集群)在 node v0.8 开始集成的内置模块,为解决单线程设计的瓶颈而生。

System Runtime :https://designfirst.io/systemruntime/,常见的运行时系统需要通过 C 或汇编而成,通过此项目可以简单构建

VM

VM 是 Node 核心模块之一,通过使用 V8 的 Virtual Machine Contexts 动态编译和执行代码,但代码的运行在上下文中是与当前隔离的,但他不是安全的,因此不要使用他来运行不受信任的代码。

不同与浏览器的沙箱环境, vm 提供了一系列的 API 用于在 V8 虚拟机中编译和运行代码,可以让 JavaScript 的代码被编译且立即运行或编译后保存运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义常量
const util = require('util');
const vm = require('vm');

// 创建一个新的 vm.Script 对象,将会编译 code 但不会运行,之后可以多次运行。
const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1;');

// 创建沙盒并绑定上下文
const sandbox = {globalVar: 1};

// 创建上下文并将沙盒(sandbox)绑定至到环境中
const contextifiedSandbox = vm.createContext(sandbox);

// 运行 vm.script 内包含的上下文对象,并返回结果
const result = script.runInContext(contextifiedSandbox);

// 是否沙盒绝对等于上下文沙盒 (true)
console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`);

// sandbox: { globalVar: 2, anotherGlobalVar: 1 }
console.log(`sandbox: ${util.inspect(sandbox)}`);

// result: 1
console.log(`result: ${util.inspect(result)}`);

vm.Script 是新增于 node.js v0.3.1 类,用于绑定任何全局对象,之后可以多次运行,也就是说我们将 sandbox 对象创建了环境的上下文,将对象绑定到环境中,因此最后输出 globalVar: 2, anotherGlobalVar: 1

这也就表明了 script.runInContextcontextifiedSandbox 上下文中运行,从而当 console.log 时,他的值已经改变了。

util.inspect 用于检查常量,除了 script.runInContext 以外,在 node.js v6.3.0 中也完全支持了更为简单的方法 vm.runlnContext

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = require('vm')

// 设置上下文全局变量为 1
const context = {globalVar: 1};

// 创建上下文方法准备该对象,以便可以调用上下文
vm.createContext(context);

// 通过 vm 方法编译 code,并运行上下文并返回结果
vm.runInContext('globalVar += 1;', context)

// globalVar:2
console.log('globalVar:' + context.globalVar);

回调\同步\异步\阻塞\非阻塞

回调函数

在计算机的设计之初,基本上都是通过异步来进行的,在此之前我们需要知道异步的相关类别

Name Info
同步 按照需求内的一步一步进行执行
异步 可以同时执行多种事情
堵塞 按照需求内一步一步进行,当一个程序没有执行,下面所排队等待的程序都处于排队状态
非堵塞 当一个程序没有执行完毕,后面所排队的程序都会进行插队

node.js 异步内的直接体现就在于回调,而回调就是,当你不知道用户何时会点击按钮,因此你为事件所定义了一个事件的处理程序,该事件会在处理时被触发调用即“回调”,我们以文件读取为例:

1
2
3
4
5
6
7
8
9
10
var fs = require('fs');

// 异步读取文件内容,通过 readFile() 缓冲整个文件
fs.readFile('package.json', function (err,data) {
if (err) {
console.log(err.stack);
return;
}
console.log(data.toString() + '\n File echo success!');
});

以上的 js 脚本中那个,主要通过 fs.readFile 函数来异步读取文件,如果文件不存在则直接通过 err 输出错误信息,如没有发生错误则会忽略 err对象进行输出,因此文件的内容就通过了回调函数输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ node vm.js 
{
"name": "demo",
"version": "1.0.0",
"description": "node demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitee.com/sif_one/node-demo"
},
"keywords": [
"null"
],
"author": "sun likun",
"license": "ISC"
}

File echo success!

假设 package.json 文件并不存在,那么在经过 s.readFile() 方法时,err 对象就会输出错误信息:

1
2
$ node vm.js 
Error: ENOENT: no such file or directory, open 'pac1kage.json'

同步和异步的区别

同步可以说是一个进程在执行某一个请求的时候,另一个进程则需要等待,等上一个请求完后,这个进程才可以执行。而异步则是另一个进程无需等待,无论其他进程状态是否,直接进程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function __test() {
setTimeout(()=> {
console.log(1000);
},1000)
}

async function test() {
for (let i=0; i<15; i++) {
try {
await __test();
} catch (e) {
console.log('a', e);
}
}
}
test();

首先我们需要注意的是,为了防止异步的出错,需要将 await 放到 try……catch 中,这就让 __testtest 两个函数同步起来,这样就会让输出的结果是__test 运行完了,test 还在等待的效果,正因这样,一个正常的运行结果才不会混乱。

堵塞

堵塞与非堵塞

1
2
3
4
5
6
7
8
9
10
11
12
var fs = require("fs")

var data = fs.readFileSync('hello.text')
fs.readFile('hello.text', (err,data)=> {
if (err != null) {
console.log('没有此文件')
return
}
})

console.log(data.toString())
console.log("程序执行结束!")

输出:

1
2
hello
程序执行结束!

在以上的code中,首先并不存在 hesllo.text 文件,处理回调错误的同时,在node中,任何回调函数中的地一个参数都为错误对象在Node官方文档中被标注为错误优先与回调的说法。

非堵塞

1
2
3
4
5
6
7
8
9
10
var fs = require("fs")

fs.readFile("hello.text", function(err,data) {
if (err !== null) {
console.log(err)
return
}
console.log(data.toString())
})
console.log('程序执行完成')

输出:

1
2
程序执行完成
hello

本文通过堵塞与非堵塞来进行演示,通常情况下堵塞代码是按照需求一步一步的执行,当一个步骤或没有完成时等待的程序处于排队状态,而非堵塞则是不当一步骤没有执行完毕后面所排队的程序都会依次进行插队即处理更加的效率

事件循环

Node.js 基本上所以的事件机制都使用类似与设计模式中的观察者模式实现,而观察者模式又是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个观察者的对象或其他对象。

观察者模式会在他本身的状态改变时发出通知,而这种呼叫各类观察者所提供的方式来实现,因此这种模式也被称之为事件处理系统。

在 node.js 中,所提供了 events 库,而他最主要的部分就是创建对象、事件处理、绑定事件以及最后的触发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var events = require('events');

// 创建事件
var eventEmitter = new events.EventEmitter();

// 创建事件处理
var connectHandler = function connected() {
console.log('1.连接成功')

// 触发 data_received 事件
eventEmitter.emit('data_received');
}

// 将 connectHandler 绑定 connection
eventEmitter.on('connection', connectHandler);

// 绑定 'data_receiver' 事件
eventEmitter.on('data_received', function () {
console.log("2.接受触发成功\n3.流程执行完毕")
});

eventEmitter.emit('connection')
1
2
3
1.连接成功
2.接受触发成功
3.流程执行完毕

上述 code 中 eventEmitter.emit() 用于触发事件,同时他也允许将任意一组参数传递给监听器函数,之后通过eventEmitter.on() 函数来注册监听器 connection ,最后通过 data_received 的事件绑定被监听器发现,从而输出整套流程。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

node.js

Node.js 是一个基于 Chrome V8 的 JavaScript 运行环境,这也让 JavaScript 成为与 PHP、Python、Perl、Ruby 等服务端语言对等的脚本语言之一。Node.js 能够在服务端运行 JavaScript 的代码和跨平台环境,因此在 node.js 出现以前,JavaScript 经常作为客户端程序设计语言使用。

ECMAScript 是 JavaScript 最初的名字,他由网景公司的 Brendan Eich 所开发的一种脚本语言标准化规范,历经 Mocha、LiveScript 最终成为 JavaScript,并在 1995年由 Sun 正式联合发布 JavaScript

获取 Node.js 的方法非常简单,我们以 Debian Linux 发行版系为例,通过 https://nodejs.org/zh-cn/ 访问 node.js 的官方站点进行下载你需要的版本(本书版本为 10),解压 tar.xz 文件后进入其 bin 目录,比昂通过 ln 建立软链接到 /usr/bin 中,最后通过其 node -v 来查看是否软链接建立成功

1
2
ln node /usr/bin/node
ln npm /usr/bin/node

JavaScript vs Node.js

JavaScript 他本身是一个客户端程序设计语言,因此针对的也是 DOM、BOM、ECMAScript 组合而成,因此针对的也是前端部分,而 Node.js 他主要通过 ECMAScript、OS、File、Net、Databases 等五个部分,分别实现了 JavaScript 语法基础、操作系统交互、文件系统、网络系统和数据库,这也是后端语言最为常用的几个部分,因此他可以脱离浏览器来运行 JavaScript 的项目。

脚本文件运行

创建名称为 hey.js 的文件后,写入 DOM 语法并通过 node.js 来进行运行

1
2
3
4
console.log("Hello,world!")

$ node hey.js
Hello,world!

交互式环境执行

1
2
3
4
5
$ node
> console.log("Hello,world!")
Hello,world!
undefined
>

构建 Node.js 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const hostname = '127.0.0.1';
const port = 8210;

const server = http.createServer((req,res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello,world!\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

至此通过 node index.js 即可运行一个简单的 node 项目,之后点击或访问 console.log 的输出来访问项目视图:http://127.0.0.1:8210/

WebStorm 如果不支持 Node.js 相关语法可以通过在 File -> Setting -> Languages & Frameworks -> JavaScript -> Libraries 内下载 node 语法支持

NPM

NPM(Node Package Manager)Node 包管理器,是 node.js 默认的依赖项之一,通常会随着 node.js 一起被安装,完全由 JavaScript 写成,最初由 lsaac Z.Schlueter 进行开发,2020 年 3 月 16 日 被 Github 收购。

node.js 维护的包管理器允许开发者下载\安装\上传他人\自己编写的第三方包到本地使用\他人使用。

NPM 的包注册(Register)也被称之为 “JavaScript 包注册表”,为了按照名称和版本来解析包,因此 nmp 采用了 CommonJS 包注册规范,这就导致了每个包都会存在一个 Json 格式的元文件。

NPM 名称以 “先到先得” 的原则进行注册,这就不会导致各个模块的名称不会发生错乱,但一旦有人撤回了自己所发布的包,就会导致依赖这个包的项目的对应功能也会出现问题。

你可以通过 nmp -v 的方式来查看版本以及是否正常安装,也可以使用 nmp install npm -g 让其进行升级。

package.json

一个项目中如果存在 package.json 文件,那么可以使用 npm install 命令来自动安装和维护所需要的依赖包,你可以通过使用 npm init 来初始化一个 package.json 文件。

Id Name
name 包名
version 包的版本号采用了语义版本号 X(住版本号).Y(次版本号).Z(补丁版本号) 三位
description 包的描述
main 指定程序的入口文件
scripts 指定了运行脚本命令的 npm 命令行缩写
repository 代码仓库位置
keywords 关键词
author 包作者的名字
license 开源许可证,默认为 (ISC)

package.json 文件可以手写也可以通过 npm init 进行生成(就是回答一些问题后自行进行创建)

从而可以快速体验工作空间以及脚本外壳的使用,因此你需要在初始化后的 CommonJS 包中添加或修改一出信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "demo",
"version": "1.0.0",
"description": "node demo",
"main": "index.js",
"scripts": {
"test": "node index.js"
},
"repository": {
"type": "git",
"url": "https://gitee.com/sif_one/node-demo"
},
"keywords": [
"null"
],
"author": "sun likun",
"license": "ISC"
}

有了 package.json 文件后,可以直接通过 npm install 进行安装所需要的包(在 dependencires 字段中写了),也可以通过指定安装依赖并写入字段的方法进行完善

npm install express --save 参数将所需依赖写入 package.json 文件中的 dependencies 属性中。

同样的还提供了写入 devDependencies 属性中的参数 --save-dev

package.json 中,如果在 script 字段内定义 js 脚本文件,即可通过 npm run test 进行运行,此时也就同等于 node index.js

当然也可以通过使用 rpm run 来查看当前项目所有的 NPM 脚本命令,因为执行该命令时会执行指定的脚本命令,因此只要是 Shell 可以运行的都可以写在 NPM 脚本中。

在大的项目中, npm run 会在 node_modules/.bin 子目录中加入变量,因此在该目录中的所有脚本都可以字节使用脚本的名称而不需要写成绝对路径。

常用命令

Id Name Info
1 npm install express 安装某一个模块
-g 全局安装这个模块(也就是说直接在 /user/local/bin 内直接关联)
2 npm uninstall name 卸载安装的所有模块
--savedependencires 为目标进行卸载
--save-devdevDependencies 为目标进行卸载
--no-save 不会将 package.json 中进行删除依赖
3 npm update express 更新模块
4 npm outdated 检查模块版本
5 npm list 查看本地安装的模块
-g 查看全局安装的模块
6 npm search express 搜索模块
7 npm cache clean 清除 NPM 本地缓存
8 npm start/restart 启动/重启模块

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布